/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { Validate } from '../../helper/Validate';
import { Attributes } from '../../nodes/Attributes';
import constants from '../../util/Constants';
import { ParseSettings } from '../ParseSettings';
import { Token } from '../Token';
export abstract class TokenTag extends Token {
  tagName: string;
  // lc version of tag name, for case insensitive tree build
  normalName: string;
  // try to get attr names and vals in one shot, vs Builder
  private readonly _attrName: string[] = []
  private _attrNameS: string | null
  private _hasAttrName = false
  private readonly _attrValue: string[] = []
  private _attrValueS: string | null
  private _hasAttrValue = false
  // distinguish boolean attribute from empty string value
  private _hasEmptyAttrValue = false
  selfClosing = false
  // start tags get attributes on construction. End tags get attributes on first new attribute (but only for parser convenience, not used).
  attributes: Attributes;

  constructor() {
    super()
  }

  tokenType(): string {
    return "TokenTag"
  }

  reset(): TokenTag {
    super.reset()
    this.tagName = null;
    this.normalName = null;
    constants.reset(this._attrName);
    this._attrNameS = null;
    this._hasAttrName = false;
    constants.reset(this._attrValue);
    this._attrValueS = null;
    this._hasEmptyAttrValue = false;
    this._hasAttrValue = false;
    this.selfClosing = false;
    this.attributes = null;
    return this
  }

  /* Limits runaway crafted HTML from spewing attributes and getting a little sluggish in ensureCapacity.
        Real-world HTML will P99 around 8 attributes, so plenty of headroom. Implemented here and not in the Attributes
        object so that API users can add more if ever required. */
  private static readonly _MaxAttributes = 512;

  newAttribute() {
    if (this.attributes === undefined || this.attributes === null) {
      this.attributes = new Attributes()
    }
    if (this._hasAttrName && this.attributes.size() < TokenTag._MaxAttributes) {
      // the tokeniser has skipped whitespace control chars, but trimming could collapse to empty for other control codes, so verify here
      let name = this._attrName.length > 0 ? this._attrName.join('') : this._attrNameS
      name = name.trim()
      if (name.length > 0) {
        let value: string | null
        if (this._hasAttrValue) {
          value = this._attrValue.length > 0 ? this._attrValue.join('') : this._attrValueS
        } else if (this._hasEmptyAttrValue) {
          value = ""
        } else {
          value = null
        }
        this.attributes.add(name, value)
      }
    }
    constants.reset(this._attrName)
    this._attrNameS = null;
    this._hasAttrName = false;

    constants.reset(this._attrValue);
    this._attrValueS = null;
    this._hasAttrValue = false;
    this._hasEmptyAttrValue = false;
  }

  hasAttributes(): boolean {
    return this.attributes !== undefined && this.attributes !== null
  }

  hasAttribute(key: string): boolean {
    return this.attributes !== undefined && this.attributes !== null && this.attributes.hasKey(key)
  }

  finaliseTag() {
    // finalises for emit
    if (this._hasAttrName) {
      this.newAttribute();
    }
  }

  getName(): string {
    // preserves case, for input into Tag.valueOf (which may drop case)
    Validate.isFalse(this.tagName === undefined || this.tagName === null || this.tagName.length === 0)
    return this.tagName;
  }

  getNormalName(): string {
    // lower case, used in tree building for working out where in tree it should go
    return this.normalName
  }

  toStringName() {
    return (this.tagName !== undefined && this.tagName !== null) ? this.tagName : "[unset]"
  }

  setName(name: string): TokenTag {
    this.tagName = name
    this.normalName = ParseSettings.normalName(this.tagName)
    return this
  }

  getTagName() {
    Validate.isFalse(this.tagName == null || this.tagName.length == 0);
    return this.tagName;
  }

  isSelfClosing(): boolean {
    return this.selfClosing
  }

  appendTagName(append: string) {
    // might have null chars - need to replace with null replacement character
    append = append.replace(this.nullChar, this.replacementChar)
    this.tagName = (this.tagName === undefined || this.tagName === null) ? append : `${this.tagName}${append}`
    this.normalName = ParseSettings.normalName(this.tagName)
  }

  appendTagNameInt(append: number) {
    this.appendTagName(String.fromCharCode(append))
  }

  appendAttributeName(append: string|number) {
    if (typeof append === 'number') {
      this.ensureAttrName();
      this._attrName.push(String.fromCharCode(append))
    } else if (typeof append === 'string') {
      // might have null chars because we eat in one pass - need to replace with null replacement character
      append = append.replace(this.nullChar, this.replacementChar)

      this.ensureAttrName();
      if (this._attrName.length === 0) {
        this._attrNameS = append
      } else {
        this._attrName.push(append)
      }
    }
  }

  appendAttributeValue(append: string | number | number[]) {
    this.ensureAttrValue()
    if (typeof append === 'number') {
      this._attrValue.push(String.fromCharCode(append))
    } else if (typeof append === 'string') {
      if (this._attrValue.length === 0) {
        this._attrValueS = append
      } else {
        this._attrValue.push(append)
      }
    } else if (Array.isArray(append)) {
      for (let codepoint of append) {
        this._attrValue.push(String.fromCharCode(codepoint))
      }
    }
  }

  setEmptyAttributeValue() {
    this._hasEmptyAttrValue = true
  }

  private ensureAttrName() {
    this._hasAttrName = true
    // if on second hit, we'll need to move to the builder
    if (this._attrNameS !== undefined && this._attrNameS !== null) {
      this._attrName.push(this._attrNameS)
      this._attrNameS = null
    }
  }

  private ensureAttrValue() {
    this._hasAttrValue = true;
    // if on second hit, we'll need to move to the builder
    if (this._attrValueS != null) {
      this._attrValue.push(this._attrValueS);
      this._attrValueS = null;
    }
  }

  public abstract toString(): string
}